《Android 基础(四十七)》FileProvider

简介

FileProvider,是ContentProvider的子类,通过构建以”content://“开头的Uri取代之前以”file://“开头的Uri,以此实现应用间的文件共享。

由来

官文Android7.0行为变更说明:

对于面向 Android 7.0 的应用,Android 框架执行的 StrictMode API 政策禁止在应用外部公开 file:// URI。如果一项包含文件 URI 的 intent 离开您的应用,则应用出现故障,并出现 FileUriExposedException 异常。

要在应用间共享文件,需要改用content:// 格式的URI,并授予 URI 临时访问权限。
实现此类操作最简单的方法就是使用FileProvider。

使用方式

定义FileProvider

FileProvider本身就能根据file生成content:// Uri,所以我们并没有必要去写一个单独的FileProvider子类。但是在某些情况下,我们可以简单的继承FileProvider,修改类名来实现与FileProvider在名字上的区分,毕竟在AndroidManifest.xml中,名字相同的provider是不被允许的。

AndroidManifest.xml中申明FileProvider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<manifest>
...
<application>
...
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
...
</application>
</manifest>

明确可用文件

上面明确来resource文件为”@xml/file_paths”。那么我们就在file_paths中明确我们的可用位置。

FileProvider只能为你事先指定的目录中的文件生成内容URI。 要指定目录,请使用< paths >元素的子元素指定其存储区域和XML路径。 例如,以下路径元素告诉FileProvider你打算请求私有文件目录下的images /子目录的内容URI

1
2
3
4
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="my_images" path="images/"/>
...
</paths>
paths元素 对应目录
< root-path/> “/“
< files-path name=”name” path=”path” /> Context.getFilesDir()
< cache-path name=”name” path=”path” /> Context.getCacheDir()
< external-path name=”name” path=”path” /> Environment.getExternalStorageDirectory()
< external-files-path name=”name” path=”path” /> Context#getExternalFilesDir(String) Context.getExternalFilesDir(null)
< external-cache-path name=”name” path=”path” /> Context.getExternalCacheDir()
< external-media-path name=”name” path=”path” /> Context.getExternalMediaDirs()(API21+)

这样看可能不太明显,随意新建一个Android工程,打印如上内容,示例工程包名为
cn.onlyloveyd.lazyshare

这里写图片描述

为File生成Content Uri

  • 创建需要用Uri表示的文件File
  • 使用getUriForFile()方法获取对应的Uri.

官方示例:

1
2
3
File imagePath = new File(Context.getFilesDir(), "images");
File newFile = new File(imagePath, "default_image.jpg");
Uri contentUri = getUriForFile(getContext(), "com.mydomain.fileprovider", newFile);

生成的Uri就是

1
content://com.mydomain.fileprovider/my_images/default_image.jpg.

临时授权Uri

给通过getUriForFile()方法返回的Uri授权的步骤:

  • 调用Context.grantUriPermission(package, Uri, mode_flags)。package为包名,Uri为需要临时授权的content Uri。mode_flags可以为FLAG_GRANT_READ_URI_PERMISSION, FLAG_GRANT_WRITE_URI_PERMISSION,根据需求而定。通过revokeUriPermission() 或者重启取消授权。
  • Intent中setData方法设置Uri
  • Intent setFlags方法设置FLAG_GRANT_READ_URI_PERMISSION 或者FLAG_GRANT_WRITE_URI_PERMISSION。
  • 发送Intent到另一个App

传递Uri到另一个应用

将content:\ Uri提供给客户端应用程序的方式很多。 一种常见的方法是客户端应用程序通过调用startActivityResult()来启动应用程序,发送一个Intent以启动一个Activity,然后通过setResult() 的方式返回给客户端。

另一种方式是通过调用Intent.setClipData()方法将content:\ Uri放入ClipData对象中,然后将该对象添加到发送给客户端应用程序的Intent中即可。

源码看看

类结构

这里写图片描述

SimplePathStrategy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
static class SimplePathStrategy implements PathStrategy {
private final String mAuthority;
private final HashMap<String, File> mRoots = new HashMap<String, File>();

SimplePathStrategy(String authority) {
mAuthority = authority;
}

/**
* Add a mapping from a name to a filesystem root. The provider only offers
* access to files that live under configured roots.
*/
//读取xml配置文件,建立名称和目录的映射表
void addRoot(String name, File root) {
//名字不能为空
if (TextUtils.isEmpty(name)) {
throw new IllegalArgumentException("Name must not be empty");
}

try {
// Resolve to canonical path to keep path checking fast
root = root.getCanonicalFile();
} catch (IOException e) {
throw new IllegalArgumentException(
"Failed to resolve canonical path for " + root, e);
}

mRoots.put(name, root);
}

@Override
public Uri getUriForFile(File file) {
String path;
try {
//获取路径
path = file.getCanonicalPath();
} catch (IOException e) {
throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
}

// Find the most-specific root path
Map.Entry<String, File> mostSpecific = null;
//遍历查找文件路径对应的名称
for (Map.Entry<String, File> root : mRoots.entrySet()) {
final String rootPath = root.getValue().getPath();
if (path.startsWith(rootPath) && (mostSpecific == null
|| rootPath.length() > mostSpecific.getValue().getPath().length())) {
mostSpecific = root;
}
}

//查询未果,说明在xml中未定义
if (mostSpecific == null) {
throw new IllegalArgumentException(
"Failed to find configured root that contains " + path);
}

// Start at first char of path under root
final String rootPath = mostSpecific.getValue().getPath();
if (rootPath.endsWith("/")) {
path = path.substring(rootPath.length());
} else {
path = path.substring(rootPath.length() + 1);
}

// Encode the tag and path separately
path = Uri.encode(mostSpecific.getKey()) + '/' + Uri.encode(path, "/");
//构建content Uri,这就是最后我们拿到的内容
return new Uri.Builder().scheme("content")
.authority(mAuthority).encodedPath(path).build();
}

@Override
public File getFileForUri(Uri uri) {
String path = uri.getEncodedPath();

//通过uri反向寻找,和上面的原理差不多,不赘述
final int splitIndex = path.indexOf('/', 1);
final String tag = Uri.decode(path.substring(1, splitIndex));
path = Uri.decode(path.substring(splitIndex + 1));

final File root = mRoots.get(tag);
if (root == null) {
throw new IllegalArgumentException("Unable to find configured root for " + uri);
}

File file = new File(root, path);
try {
file = file.getCanonicalFile();
} catch (IOException e) {
throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
}

if (!file.getPath().startsWith(root.getPath())) {
throw new SecurityException("Resolved path jumped beyond configured root");
}

return file;
}
}

parsePathStrategy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
private static PathStrategy parsePathStrategy(Context context, String authority)
throws IOException, XmlPullParserException {
final SimplePathStrategy strat = new SimplePathStrategy(authority);

//获取Provider信息
final ProviderInfo info = context.getPackageManager()
.resolveContentProvider(authority, PackageManager.GET_META_DATA);
// 获取"android.support.FILE_PROVIDER_PATHS"对应的xml文件解析对吸纳个;
final XmlResourceParser in = info.loadXmlMetaData(
context.getPackageManager(), META_DATA_FILE_PROVIDER_PATHS);
if (in == null) {
throw new IllegalArgumentException(
"Missing " + META_DATA_FILE_PROVIDER_PATHS + " meta-data");
}

int type;
while ((type = in.next()) != END_DOCUMENT) {
if (type == START_TAG) {
final String tag = in.getName();

final String name = in.getAttributeValue(null, ATTR_NAME);
String path = in.getAttributeValue(null, ATTR_PATH);

File target = null;
//"root-path"
if (TAG_ROOT_PATH.equals(tag)) {
target = DEVICE_ROOT;
//"files-path"
} else if (TAG_FILES_PATH.equals(tag)) {
target = context.getFilesDir();
//"cache-path"
} else if (TAG_CACHE_PATH.equals(tag)) {
target = context.getCacheDir();
//"external-path"
} else if (TAG_EXTERNAL.equals(tag)) {
target = Environment.getExternalStorageDirectory();
//"external-files-path"
} else if (TAG_EXTERNAL_FILES.equals(tag)) {
File[] externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null);
if (externalFilesDirs.length > 0) {
//取数组中的第一个
target = externalFilesDirs[0];
}
// "external-cache-path"
} else if (TAG_EXTERNAL_CACHE.equals(tag)) {
File[] externalCacheDirs = ContextCompat.getExternalCacheDirs(context);
if (externalCacheDirs.length > 0) {
target = externalCacheDirs[0];
}
// "external-media-path" L版本以上才有
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& TAG_EXTERNAL_MEDIA.equals(tag)) {
File[] externalMediaDirs = context.getExternalMediaDirs();
if (externalMediaDirs.length > 0) {
target = externalMediaDirs[0];
}
}

if (target != null) {
strat.addRoot(name, buildPath(target, path));
}
}
}
return strat;
}

从上面这个方法可以很直观的了解到在AndroidManifest.xml文件中定义provider以及对应的共享文件路径定义xml的解析过程。以及xml中tag与真实文件路径的对应关系。

使用场景

拍照

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
String filename = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.CHINA)
.format(new Date()) + ".png";
File file = new File(Environment.getExternalStorageDirectory(), filename);
mCurrentPhotoPath = file.getAbsolutePath();

Uri fileUri;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
fileUri = getUriForFile(context,
context.getPackageName() +".fileprovider", file);
} else {
fileUri = Uri.fromFile(file);
}

takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PHOTO);
}
……
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK && requestCode == REQUEST_CODE_TAKE_PHOTO) {
mIvPhoto.setImageBitmap(BitmapFactory.decodeFile(mCurrentPhotoPath));
}
// else tip?

}

应用安装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 需要自己修改安装包路径
File file = new File(Environment.getExternalStorageDirectory(),
"/onlyloveyd/base.apk");
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.setDataAndType(getUriForFile(context, file), "application/vnd.android.package-archive");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
} else {
intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
}
startActivity(intent);
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×